//
// Created by zerax on 2020/10/26.
//

#include <SDL2/SDL_log.h>
#include "vertex_array.hh"

using namespace std;

VertexArray::VertexArray(bool hasIndex) {
    glGenVertexArrays(1, &m_rawHandle);
    glGenBuffers(1, &m_vertexBuffer);
    if (hasIndex) {
        GLuint ibo;
        glGenBuffers(1, &ibo);
        m_indexBuffer = ibo;
    }
}

VertexArray::VertexArray(VertexArray &&vao) noexcept {
    m_rawHandle = vao.m_rawHandle;
    m_vertexBuffer = vao.m_vertexBuffer;
    m_indexBuffer = vao.m_indexBuffer;
    m_vertexCount = vao.m_vertexCount;
    memset(reinterpret_cast<void*>(&vao), 0, sizeof(VertexArray));
}

VertexArray &VertexArray::operator=(VertexArray &&vao) noexcept {
    m_rawHandle = vao.m_rawHandle;
    m_vertexBuffer = vao.m_vertexBuffer;
    m_indexBuffer = vao.m_indexBuffer;
    m_vertexCount = vao.m_vertexCount;
    memset(reinterpret_cast<void*>(&vao), 0, sizeof(VertexArray));
    return *this;
}

VertexArray::~VertexArray() {
    if (m_rawHandle == 0) return;
    SDL_Log("VAO deleted");

    glDeleteVertexArrays(1, &m_rawHandle);
    glDeleteBuffers(1, &m_vertexBuffer);
    if (m_indexBuffer.has_value())
        glDeleteBuffers(1, &*m_indexBuffer);
}

void VertexArray::draw() const {
    glBindVertexArray(m_rawHandle);

    if (m_indexBuffer.has_value()) {
        glDrawElements(GL_TRIANGLES, m_vertexCount, GL_UNSIGNED_INT, nullptr);
    } else {
        glDrawArrays(GL_TRIANGLES, 0, m_vertexCount);
    }
}

VertexArrayBuilder::VertexArrayBuilder(unsigned int loc, std::vector<glm::vec3>& positions):
    m_positions(loc, &positions), m_texCoords(), m_triangles() {
}

VertexArrayBuilder &VertexArrayBuilder::withTexCoords(unsigned int loc, std::vector<glm::vec2>& texCoords) {
    m_texCoords = make_pair(loc, &texCoords);
    return *this;
}


VertexArrayBuilder &VertexArrayBuilder::calculateNormal(unsigned int location) {
    if (!m_normals && m_triangles) {
        vector<glm::vec3> normals(m_positions.second->size());
        auto& triangles = *m_triangles.value();
        auto& positions = *m_positions.second;
        for (auto i = 0; i < triangles.size(); i += 3) {
            auto a = positions[triangles[i]];
            auto b = positions[triangles[i + 1]];
            auto c = positions[triangles[i + 2]];

            auto normal = glm::normalize(glm::cross(b - a, c - a));

            normals[triangles[i]] = normal;
            normals[triangles[i + 1]] = normal;
            normals[triangles[i + 2]] = normal;
        }

        for (auto& normal: normals) {
            SDL_Log("(%f, %f, %f)", normal.x, normal.y, normal.z);
        }
        m_normals = make_pair(location, move(normals));
    }
    return *this;
}

VertexArrayBuilder &VertexArrayBuilder::withTriangles(std::vector<GLuint>& triangles) {
    m_triangles = &triangles;
    return *this;
}

VertexArray VertexArrayBuilder::build(VertexArrayType type) {
    VertexArray vao(m_triangles.has_value());
    reset(vao, type);
    return vao;
}

void VertexArrayBuilder::reset(VertexArray &vao, VertexArrayType type) {
    using glm::vec2;
    using glm::vec3;

    auto& [positionsLocation, positions] = m_positions;
    auto positionsLength = positions->size() * sizeof(vec3);
    auto bufferSize = positionsLength;

    if (m_texCoords) {
        bufferSize += m_texCoords->second->size() * sizeof(vec2);
    }

    if (m_normals) {
        bufferSize += m_normals->second.size() * sizeof(vec3);
    }

    GLenum bufferType;

    switch (type) {
        case VertexArrayType::Static:
            bufferType = GL_STATIC_DRAW;
            break;
        case VertexArrayType::Dynamic:
            bufferType = GL_DYNAMIC_DRAW;
            break;
        case VertexArrayType::Streaming:
            bufferType = GL_STREAM_DRAW;
            break;
    }

    auto currentOffset = 0;

    glBindVertexArray(vao.m_rawHandle);
    glBindBuffer(GL_ARRAY_BUFFER, vao.m_vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, bufferSize, nullptr, bufferType);

    glBufferSubData(GL_ARRAY_BUFFER, currentOffset, positions->size() * sizeof(vec3), positions->data());
    currentOffset += positions->size() * sizeof(vec3);
    if (m_texCoords) {
        auto& [texCoordsLocation, texCoords] = m_texCoords.value();
        glBufferSubData(GL_ARRAY_BUFFER, currentOffset, texCoords->size() * sizeof(vec2), texCoords->data());

        glVertexAttribPointer(texCoordsLocation, vec2::length(), GL_FLOAT, GL_FALSE, sizeof(vec2),
                              reinterpret_cast<void *>(currentOffset));
        glEnableVertexAttribArray(texCoordsLocation);

        currentOffset += texCoords->size() * sizeof(vec2);
    }

    if (m_normals) {
        auto& [normalsLocation, normals] = m_normals.value();
        glBufferSubData(GL_ARRAY_BUFFER, currentOffset, normals.size() * sizeof(vec3), normals.data());


        glVertexAttribPointer(normalsLocation, vec3::length(), GL_FLOAT, GL_FALSE, sizeof(vec3),
                              reinterpret_cast<void*>(currentOffset));
        glEnableVertexAttribArray(normalsLocation);

        currentOffset += normals.size() * sizeof(vec3);
    }

    glVertexAttribPointer(positionsLocation, vec3::length(), GL_FLOAT, GL_FALSE, sizeof(vec3), nullptr);
    glEnableVertexAttribArray(positionsLocation);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    if (m_triangles) {
        auto& indices = m_triangles.value();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, *vao.m_indexBuffer);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices->size() * sizeof(GLuint), indices->data(), bufferType);
    }

    glBindVertexArray(0);

    vao.m_vertexCount = positionsLength;
}
